/** Include configure Header **/
#include "config.h"

#include <exception>
#include <algorithm>

/** Filesystem Header**/
#include "Filesystem.h"

/** Database Header **/
#include "PGSQLDatabase.h"
#include "MySQLDatabase.h"
#include "OracleDatabase.h"

/** Fileformats **/
#include "MemoryFileFormat.h"
#include "GMLFileFormat.h"
#include "WKTFileFormat.h"
#include "SHPFileFormat.h"

bool Filesystem::isDebug = false;
boost::mutex Filesystem::debugLock;

/**
 *
 **/
Filesystem::Filesystem(PString type, PString host, PString port, PString database, PString username, PString password, PString cacheDirName, PString geomFieldName, int argc, char* argv[], size_t cacheLifeTime, bool debug)
{
    Filesystem::isDebug = debug;

    this->geometryFieldName = geomFieldName;
    this->dbc = NULL;

	#ifdef ENABLE_MYSQL
    if(type->compare("mysql") == 0) {
        this->dbc = new MySQLDatabase(host, port, database, username, password);
    }
    #endif

    #ifdef ENABLE_POSTGRESQL
    if(type->compare("pgsql") == 0) {
        this->dbc = new PGSQLDatabase(host, port, database, username, password);
    }
    #endif

    #ifdef ENABLE_ORACLE
    if(type->compare("oracle") == 0) {
        this->dbc = new OracleDatabase(host, port, database, username, password);
    }
    #endif
    if(this->dbc == NULL)
    {
        std::cout << "error: Database "+*type+" not supported" << std::endl;
        exit(EXIT_FAILURE);
    }

    String dir = argv[argc-1];
	dir.append(PATH_SEPERATOR);

	this->mountDir = dir;
	this->cacheDir = dir+*cacheDirName+PATH_SEPERATOR;
	this->cacheDirName = *cacheDirName;
	this->internCacheDir = PATH_SEPERATOR+*cacheDirName+PATH_SEPERATOR;
    this->cacheLifeTime = cacheLifeTime;

	std::cout << "mount dir: " << this->mountDir << std::endl;
	std::cout << "cache dir: " << this->cacheDir << std::endl;
}


/**
 *
 **/
int Filesystem::mount(int argc, char* argv[])
{
    return geodbfs_init(argc, argv, this);
}

/**
 *
 **/
Filesystem::~Filesystem(){
    for (MemoryFileMap::iterator it=this->getMemoryFileMap()->begin() ; it != this->getMemoryFileMap()->end(); it++ )
    {
        free(it->second.fileContent);
    }
	delete this->dbc;
}

/**
 *
 **/
PDatabase Filesystem::getDBConnection()
{
    return this->dbc;
}

/**
 *
 **/
PMemoryFileMap Filesystem::getMemoryFileMap()
{
    return &this->memoryFileCache;
}


/**
 *
 **/
void Filesystem::addMemoryFile(PFSInfo info)
{
	String oldPath = info->path;
	MemoryFile file;
	file.size = 0;
	if(info->isCache) {
		file.memSize = 1048576; // Default 1 MB
	} else {
		TableInfo table = info->filesystem->getDBConnection()->getTableInfoOf( &info->database, &info->schema, &info->table, OPTION_TABLE_INFO);
		file.memSize = (size_t)((double)table.tableSize * info->sizeParam);
		info->path = this->makeCacheFileName(info);
	}
	file.fileContent = malloc(file.memSize);
	memset(file.fileContent , 0, file.memSize);
	file.read = false;
	file.write = true;
	file.realPath = info->path;
	file.realFileName = info->file;
	this->getMemoryFileMap()->insert( make_pair(info->path, file ) );
	info->path = oldPath;
}

/**
 *
 **/
PMemoryFile Filesystem::getMemoryFile(String &path, bool isCache)
{
	try
	{
	    String filename;
	    if(isCache) {
	        filename = path;
	    } else {
            filename = path.substr(1, path.size());
            boost::algorithm::replace_all(filename, PATH_SEPERATOR, "_");
            filename.insert(0, this->getInternCacheDir());
	    }
		MemoryFileMap::iterator it = this->getMemoryFileMap()->find(filename);

		if(it != this->getMemoryFileMap()->end())
		{
			return &it->second;
		}
		else
		{
			return NULL;
		}
	} catch(std::bad_exception& e) {
		std::cout << "error: " << e.what() << std::endl;
		return NULL;
	}
}

/**
 *
 **/
PFileOperationRegistryMap Filesystem::getFileOperationRegistryMap()
{
    return &this->operationRegistry;
}

/**
 *
 **/
void Filesystem::addFileOperationRegistry(String database, String schema, String table) // String type,
{
	FileOperationRegistry value;
	value.hasWorkerThread = false;
	value.createMode = true;
	value.created = false;
	value.isChanged = false;
	value.hasWriteMainThread = false;
	//this->getFileOperationRegistryMap()->insert( make_pair(type+"_"+database+"_"+schema+"_"+table, value));
	this->getFileOperationRegistryMap()->insert( make_pair(database+"_"+schema+"_"+table, value));
}

/**
 *
 **/
void Filesystem::removeFileOperationRegistry(String database, String schema, String table) //String type,
{
	this->getFileOperationRegistryMap()->erase(database+"_"+schema+"_"+table); // type+"_"+
}
/**
 *
 **/
PFileOperationRegistry Filesystem::getFileOperationRegistry(String database, String schema, String table) //String operation,
{
    FileOperationRegistryMap::iterator it = this->getFileOperationRegistryMap()->find(database+"_"+schema+"_"+table);
    if(it != this->getFileOperationRegistryMap()->end())
    {
        return &it->second;
    }
    else
    {
        return NULL;
    }
}


int Filesystem::openDir(PFSInfo info)
{
	if((info->isCache && info->level > 3) || (info->isCache && info->level > 1) || (!info->isCache && info->level > 4))
	{
	    return -ENOENT;
	}
	else if(info->isDirectory && info->level >= 3)
	{
		String file1 = info->path;
		boost::to_lower(file1);
		if(file1.compare(info->extension) != 0 && (info->extension.compare("gfs") == 0 || info->extension.compare("xsd") == 0 || info->extension.compare("gml") == 0 || info->extension.compare("qix") == 0 || info->extension.compare("shp") == 0 || info->extension.compare("dbf") == 0 || info->extension.compare("shx") == 0 || info->extension.compare("gml") == 0 || info->extension.compare("xsd") == 0 || info->extension.compare("wkt") == 0 || info->extension.compare("prj") == 0 || info->extension.compare("cpg") == 0))
		{
			return -ENOENT;
		}
	}
	return 0;
}

/**
 *
 **/
int Filesystem::readDir(PFSInfo info)
{
    if(Filesystem::isIgnoreFile(info)) return -ENOENT;

    this->listDir(".",  info);
    this->listDir("..", info);

    if(info->level == 0) this->listDir(this->getCacheDirName(), info);
    if(info->isCache)
	{
		MemoryFileMap::iterator it;
		for ( it=this->getMemoryFileMap()->begin() ; it != this->getMemoryFileMap()->end(); it++ )
		{
			try {
				this->listFile(getPathElements(it->first).at(2), info, it->second.size);
			} catch(...){
			}
		}

		return 0;
	}


    if(info->level < 3)
    {
        PQuery result = NULL;
        switch(info->level) {
            case 0: // root dir, list Databases
                result = this->getDBConnection()->listDatabases();
                break;
            case 1: // list Schemas
                result = this->getDBConnection()->listSchemata( &info->database );
                break;
            case 2: // list Tables
                result = this->getDBConnection()->listTables( &info->database, &info->schema);
                break;
        }
        while(result->hasMore()) {
            this->listDir(result->fetch()->getFieldValue(0), info);
        }
        delete result;
    }
    else // Files
    {
		Filesystem::debug("\nlist file: "+info->database+"\n");
        TableInfo table = this->getDBConnection()->getTableInfoOf(&info->database, &info->schema, &info->table, OPTION_TABLE_INFO);
        double filesize = table.tableSize;
        if(table.hasGeometry)
        {
            if( find (unlinkList.begin(), unlinkList.end(), info->path+PATH_SEPERATOR+info->table+".gml") == unlinkList.end())
                this->listFile(info->table+".gml", info, (size_t)(filesize * GML_FILESIZE_PARAM));

            if( find (unlinkList.begin(), unlinkList.end(), info->path+PATH_SEPERATOR+info->table+".xsd") == unlinkList.end())
                this->listFile(info->table+".xsd", info, (size_t)(filesize));

            if( find (unlinkList.begin(), unlinkList.end(), info->path+PATH_SEPERATOR+info->table+".shp") == unlinkList.end())
                this->listFile(info->table+".shp", info, (size_t)(filesize * SHP_FILESIZE_PARAM));

            if( find (unlinkList.begin(), unlinkList.end(), info->path+PATH_SEPERATOR+info->table+".shx") == unlinkList.end())
                this->listFile(info->table+".shx", info, table.countRows*4+100);

            if( find (unlinkList.begin(), unlinkList.end(), info->path+PATH_SEPERATOR+info->table+".wkt") == unlinkList.end())
                this->listFile(info->table+".wkt", info, (size_t)(filesize * WKT_FILESIZE_PARAM));

            if(table.hasSRID && find (unlinkList.begin(), unlinkList.end(), info->path+PATH_SEPERATOR+info->table+".prj") == unlinkList.end())
            {
                    this->listFile(info->table+".prj", info, 1024);
            }
        }
        if( find (unlinkList.begin(), unlinkList.end(), info->path+PATH_SEPERATOR+info->table+".dbf") == unlinkList.end())
            this->listFile(info->table+".dbf", info, (size_t)(filesize * SHP_FILESIZE_PARAM));
    }
    return 0;
}

/**
 *
 **/
int Filesystem::open(PFSInfo info) {
    if(Filesystem::isIgnoreFile(info)) return -ENOENT;

	if(!info->isDirectory && !info->isCache && info->writeMode == false && info->rwMode == false )
	{
		TableInfo table = this->getDBConnection()->getTableInfoOf(&info->database, &info->schema, &info->table, OPTION_TABLE_INFO);
        bool var1 = (table.hasGeometry && (info->extension.compare("shp") == 0 || info->extension.compare("shx") == 0 || info->extension.compare("gml") == 0 || info->extension.compare("xsd") == 0 || info->extension.compare("wkt") == 0 || (table.hasSRID && info->extension.compare("prj") == 0))) || info->extension.compare("dbf") == 0;
        if(!var1)
        {
                return -ENOENT;
        }
	}

    if(info->createMode == true && find(this->unlinkList.begin(), this->unlinkList.end(), info->path) != this->unlinkList.end())
    {
        StringVector::iterator it = find(this->unlinkList.begin(), this->unlinkList.end(), info->path);
        this->unlinkList.erase(it);
    }

    PFileHandler fh = new FileHandler;
    fh->deleteOnClose = false;
	fh->writeMode = info->writeMode == true || info->rwMode == true;
	if(info->isCache) {
		fh->file = new MemoryFileFormat(info);
	}
	else if(info->extension.compare("wkt") == 0) {
		fh->file = new WKTFileFormat(info);
	}
	else if(info->extension.compare("shp") == 0 || info->extension.compare("dbf") == 0 || info->extension.compare("shx") == 0 || info->extension.compare("prj") == 0) {
		fh->file = new SHPFileFormat(info);
	}
	else if(info->extension.compare("gml") == 0 || info->extension.compare("xsd") == 0 || info->extension.compare("gfs") == 0) {
		fh->file = new GMLFileFormat(info);
	}
	else if(info->isDirectory == false) {
		fh->file = new MemoryFileFormat(info);
	}
	else {
		fh->file = NULL;
	}

	this->setFileHandler(info, fh);
    return 0;
}


/**
 *
 **/
int Filesystem::truncate(PFSInfo info)
{
    PFileHandler fh = this->getFileHandler(info);
    if(fh != NULL && fh->file != NULL){
        return fh->file->truncate(info);
    }
    return 0;
}

// Set only bool write = true, the file with write by the close Methode
/**
 *
 **/
int Filesystem::write(PFSInfo info) {
    PFileHandler fh = this->getFileHandler(info);
    if(fh->file != NULL){
        //boost::mutex::scoped_lock lock( this->writeLock );
        int status = fh->file->write(info);
        //lock.unlock();
        return status;
    }
    return 0;
}

/**
 *
 **/
int Filesystem::read(PFSInfo info) {
    if(Filesystem::isIgnoreFile(info)) return -ENOENT;
    PFileHandler fh = this->getFileHandler(info);
	if(fh != NULL){
		if(fh->file != NULL) {
		    //boost::mutex::scoped_lock lock( this->readLock);
			int status = fh->file->read(info);
			//lock.unlock();
			return status;
		}
	}
    return 0;
}

/**
 *
 **/
int Filesystem::close(PFSInfo info)
{
	int err = 0;
    PFileHandler fh = this->getFileHandler(info);
	if(fh != NULL)
	{
		if(fh->file != NULL)
		{
			err = fh->file->close(info);
			delete fh->file;
		}
		delete fh;
	}
	return err;
}


bool Filesystem::isIgnoreFile(PFSInfo info)
{
	String database = info->database;
	String path = info->path;
	boost::to_lower(database);
	boost::to_lower(path);
    return  database.compare(".trash") == 0 ||
            database.compare(".trash-1000") == 0 ||
            database.compare(".hidden") == 0 ||
            database.compare(".xdg-volume-info") == 0 ||
            database.compare("desktop.ini") == 0 ||
			database.compare("autorun.inf") == 0 ||
            (path.size() > 11 && path.substr(path.size()-11).compare("desktop.ini")== 0) ||
            (path.size() > 8  && path.substr(path.size()-8 ).compare(".dropbox")   == 0) ||
            (path.size() > 7  && path.substr(path.size()-7 ).compare("tic.dbf")    == 0) ||
            (path.size() > 10 && path.substr(path.size()-10).compare("dbltic.dbf") == 0) ||
            (path.size() > 7  && path.substr(path.size()-7 ).compare(".hidden")    == 0);
}

bool Filesystem::isSupportedFile(PFSInfo info, bool hasGeometry, bool hasSRID)
{
    return (hasGeometry && (info->extension.compare("shp") == 0 ||
                           info->extension.compare("shx") == 0 ||
                           info->extension.compare("gml") == 0 ||
                           info->extension.compare("xsd") == 0 ||
                           info->extension.compare("qix") == 0 ||
                           info->extension.compare("cpg") == 0 || // code page file
                           info->extension.compare("wkt") == 0))
                           ||
                           info->extension.compare("dbf") == 0
                           ||
            (hasSRID    &&  info->extension.compare("prj") == 0);
}

/**
 *
 **/
int Filesystem::getattr(PFSInfo info)
{
    if(Filesystem::isIgnoreFile(info) || info->level > 4 || (info->isCache && info->level > 3)) return -ENOENT;

    if(info->isCache)
    {
        if(info->level < 2) {
            this->setDirAttribute( info , false);
        }
        else {
            this->setFileAttribute( info, 10000 );
        }
    }
    else
    {
        if(info->isDirectory)
        {
            String file1 = info->path;
            boost::to_lower(file1);
            if(info->level == 3 && file1.compare(info->extension) != 0)
            {
				String comparePath = this->makeCacheFileName(info);
                for (MemoryFileMap::iterator it = this->getMemoryFileMap()->begin() ; it != this->getMemoryFileMap()->end(); it++ )
                {
                    if(it->first.compare(comparePath) == 0)
                    {
                        this->setFileAttribute( info, 10000 );
                        return 0;
                    }
                }
				return -ENOENT;
            }

			if(!this->getDBConnection()->exists(&info->database, &info->schema, &info->table)) return -ENOENT;
            this->setDirAttribute( info , !(info->level > 1) );//read only or chmod 777
        }
        else
        {
            for (unsigned int i=0; i<this->unlinkList.size(); i++) {
                if(info->path.compare(this->unlinkList[i]) == 0)
                {
                    this->setFileAttribute( info, 10000 );
                    return -ENOENT;
                }
            }

            PFileHandler fh = this->getFileHandler(info);
			if(fh != NULL && fh->writeMode == true)
			{
				String search = this->makeCacheFileName(info);
				for (MemoryFileMap::iterator it = this->getMemoryFileMap()->begin() ; it != this->getMemoryFileMap()->end(); it++ )
                {
                    if(it->first.compare(search) == 0)
                    {
                        this->setFileAttribute( info, 10000 );
                        return 0;
                    }
                }
				return -ENOENT;
			}
            TableInfo table = this->getDBConnection()->getTableInfoOf(&info->database, &info->schema, &info->table, OPTION_TABLE_INFO);
            if(!Filesystem::isSupportedFile(info, table.hasGeometry, table.hasSRID)) return -ENOENT;
            size_t filesize = 0;
            if(info->extension.compare("gml") == 0)
            {
                filesize = (size_t)((double)table.tableSize*GML_FILESIZE_PARAM);
            }
            else if(info->extension.compare("shx") == 0)
            {
                filesize = table.countRows*4+100;
            }
            else
            {
                filesize = (size_t)((double)table.tableSize*DEFAULT_FILESIZE_PARAM);
            }
            this->setFileAttribute( info, filesize );
        }
    }
    return 0;
}

int Filesystem::unlink(PFSInfo info) {
    if(info->isCache)
    {
        PMemoryFile file = this->getMemoryFile(info->path, true);
        if(file != NULL)
        {
            free(file->fileContent);
            this->getMemoryFileMap()->erase(info->path);
            return 0;
        }
        return -1;
    }
    else
    {
        if(info->level == 4)
        {
			FSInfo newinfo;
			newinfo.isCache = true;
			newinfo.path = info->path;
			newinfo.path = this->makeCacheFileName(&newinfo);
			this->unlink(&newinfo);
            this->unlinkList.push_back(info->path);
            return 0;
        }
    }
	return -1;
}


String Filesystem::getMountDir()
{
    return this->mountDir;
}

String Filesystem::getCacheDir()
{
    return this->cacheDir;
}

String Filesystem::getInternCacheDir()
{
    return this->internCacheDir;
}

String Filesystem::getCacheDirName()
{
	return this->cacheDirName;
}

StringVector Filesystem::getPathElements(String path)
{
	StringVector strs;
	boost::split(strs, path, boost::is_any_of( PATH_SEPERATOR ));
	try{
		if(strs.at(1).compare("") == 0) strs.erase(strs.begin()+1);
	} catch(...) {
	}

	return strs;
}

String Filesystem::getNameFromFile(String &filename)
{
    return filename.substr( 0, filename.rfind(".") );
}

String Filesystem::makeCacheFileName(PFSInfo info)
{
    String filename = info->path.substr(1, info->path.size());
    boost::algorithm::replace_all(filename, PATH_SEPERATOR, "_");
    filename.insert(0, this->getInternCacheDir());
    return filename;
}

void Filesystem::debug(String msg)
{
    if(Filesystem::isDebug)
    {
        boost::mutex::scoped_lock lock( Filesystem::debugLock );
        std::cout << msg << std::endl;
        lock.unlock();
    }
}

#ifdef WINDOWS
FSInfo Filesystem::createFSInfo(String func, LPCWSTR &path, LPVOID readBuffer, LPCVOID writeBuffer, DWORD bufferSize, LPDWORD readLength, LONGLONG offset, PFillFindData filler,LPBY_HANDLE_FILE_INFORMATION attr, PDOKAN_FILE_INFO fi, PFilesystem filesystem)
#else
FSInfo Filesystem::createFSInfo(String func, const char* path, void*readBuffer, const char* writeBuffer, fuse_fill_dir_t filler, off_t offset, size_t bufferSize, struct stat *attr, struct fuse_file_info *fi, PFilesystem filesystem)
#endif
{
    FSInfo info;
    #ifdef WINDOWS
    //Unicode WideCharacter String to String
    size_t convertedChars = 0;
    size_t sizeInBytes = ((wcslen(path) + 1) * 2);

    char *npath = (char *)malloc(sizeInBytes);

    wcstombs_s(&convertedChars, npath, sizeInBytes, path, sizeInBytes);

    info.path = npath;
    free(npath);
    #else
	info.path = path;
	#endif

	StringVector pathArray = Filesystem::getPathElements(info.path);

	info.readBuffer  = readBuffer;
	info.writeBuffer = writeBuffer;
	info.filler		 = filler;
	info.offset		 = offset;
	info.fi			 = fi;
	info.size		 = bufferSize;
	info.isCache	 = false;
	info.attributes  = attr;
	info.isDirectory = false;
	info.database	 = "";
	info.schema		 = "";
	info.table		 = "";
	info.file		 = "";
	info.extension	 = info.path.substr( info.path.rfind(".")+1, info.path.size() );
	info.sizeParam   = DEFAULT_FILESIZE_PARAM;
	info.filesystem  = filesystem;
	info.writeMode   = false;
	info.readMode    = false;
	info.rwMode      = false;
	info.createMode  = false;
	info.appendMode  = false;

    boost::to_lower(info.extension);


    /* 0 = root (list database)
     * 1 = schemata (list schema)
     * 2 = tables (list table)
     * 3 = files   (list files) */

	info.level  = pathArray.size() - 1;

    Filesystem::debug( func + ": " + info.path);

    try{
        if(info.level > 0)
        {
            info.database = pathArray.at(1);
            info.isCache  = info.database.compare(filesystem->getCacheDirName()) == 0;
        }
        if(info.level > 1)
        {
            info.schema   = pathArray.at(2);
        }
        if(info.level > 2)
		{
			if(info.level == 3 && info.extension.compare(info.path) != 0)
			{
				info.file  = pathArray.at(3);
				info.table = Filesystem::getNameFromFile(pathArray.at(3));
			}
			else info.table    = pathArray.at(3);
		}
        if(info.level > 3)
        {
            info.file      = pathArray.at(4);
        }
        if(info.isCache && info.level > 1)
        {
            info.table    = pathArray.at(2);
        }
        info.isDirectory = (info.isCache && info.level == 1) || (info.level < 4 && !info.isCache);
    }catch(std::exception e)
    {
        std::cout << "error: " << e.what() << std::endl;
    }
	return info;
}

String Filesystem::getGeomFieldName()
{
    return *this->geometryFieldName;
}


size_t Filesystem::getCacheLifeTime()
{
    return this->cacheLifeTime;
}
